import json
import os
import time
import random
import openpyxl
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.action_chains import ActionChains
import tkinter as tk
from tkinter import ttk, scrolledtext
from threading import Thread

class JDItemCrawlerApp:
    def __init__(self, root):
        self.root = root
        self.root.title("京东商品信息提取工具")
        self.root.geometry("900x700")
        self.root.configure(bg="#f5f5f5")
        
        # 设置Chrome选项
        self.chrome_options = Options()
        self.chrome_options.add_argument("--disable-gpu")
        self.chrome_options.add_argument("--no-sandbox")
        self.chrome_options.add_argument("--disable-dev-shm-usage")
        self.chrome_options.add_argument("--disable-software-rasterizer")
        self.chrome_options.add_argument("--disable-webgl")
        self.chrome_options.add_argument("--disable-features=VizDisplayCompositor")
        self.chrome_options.add_argument("--ignore-certificate-errors")
        self.chrome_options.add_argument("--disable-blink-features=AutomationControlled")
        
        # 随机User-Agent
        user_agents = [
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:129.0) Gecko/20100101 Firefox/129.0",
            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15"
        ]
        self.chrome_options.add_argument(f"user-agent={random.choice(user_agents)}")
        
        # 禁用自动化特征
        self.chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"])
        self.chrome_options.add_experimental_option('useAutomationExtension', False)
        
        self.driver = None
        self.cookie_file = r"G:\py\美团\jd_cookies.json"
        self.stealth_js_path = r"G:\py\美团\stealth.min.js"
        self.login_url = "https://passport.jd.com/uc/login"
        self.search_url = "https://search.jd.com/Search?keyword=%E8%A5%BF%E7%93%9C%E9%9C%9C&enc=utf-8"
        self.output_file = "jd_data.xlsx"
        self.setup_ui()
        
    def setup_ui(self):
        title_label = tk.Label(self.root, text="京东商品信息提取工具", font=("Arial", 18, "bold"), 
                              fg="#e1251b", bg="#f5f5f5")
        title_label.pack(pady=20)
        
        desc_label = tk.Label(self.root, text="提取京东搜索页面商品的标题、价格、销量和店铺名称，并导出到Excel（使用stealth.min.js防检测）", 
                             font=("Arial", 12), bg="#f5f5f5", fg="#666")
        desc_label.pack(pady=5)
        
        url_frame = tk.Frame(self.root, bg="#f5f5f5")
        url_frame.pack(pady=10, padx=20, fill="x")
        
        tk.Label(url_frame, text="京东搜索URL:", font=("Arial", 10), bg="#f5f5f5").pack(anchor="w")
        self.url_var = tk.StringVar(value=self.search_url)
        url_entry = tk.Entry(url_frame, textvariable=self.url_var, font=("Arial", 10), width=70)
        url_entry.pack(pady=5, fill="x")
        
        cookie_frame = tk.Frame(self.root, bg="#f5f5f5")
        cookie_frame.pack(pady=10, padx=20, fill="x")
        
        tk.Label(cookie_frame, text="Cookies文件路径:", font=("Arial", 10), bg="#f5f5f5").pack(anchor="w")
        self.cookie_var = tk.StringVar(value=self.cookie_file)
        cookie_entry = tk.Entry(cookie_frame, textvariable=self.cookie_var, font=("Arial", 10), width=70, state="readonly")
        cookie_entry.pack(pady=5, fill="x")
        
        stealth_frame = tk.Frame(self.root, bg="#f5f5f5")
        stealth_frame.pack(pady=10, padx=20, fill="x")
        
        tk.Label(stealth_frame, text="Stealth JS路径:", font=("Arial", 10), bg="#f5f5f5").pack(anchor="w")
        self.stealth_var = tk.StringVar(value=self.stealth_js_path)
        stealth_entry = tk.Entry(stealth_frame, textvariable=self.stealth_var, font=("Arial", 10), width=70, state="readonly")
        stealth_entry.pack(pady=5, fill="x")
        
        output_frame = tk.Frame(self.root, bg="#f5f5f5")
        output_frame.pack(pady=10, padx=20, fill="x")
        
        tk.Label(output_frame, text="输出Excel文件:", font=("Arial", 10), bg="#f5f5f5").pack(anchor="w")
        self.output_var = tk.StringVar(value=self.output_file)
        output_entry = tk.Entry(output_frame, textvariable=self.output_var, font=("Arial", 10), width=70, state="readonly")
        output_entry.pack(pady=5, fill="x")
        
        btn_frame = tk.Frame(self.root, bg="#f5f5f5")
        btn_frame.pack(pady=15)
        
        self.start_btn = tk.Button(btn_frame, text="开始提取", command=self.start_crawling, 
                                  bg="#e1251b", fg="white", font=("Arial", 12), width=15)
        self.start_btn.pack(side="left", padx=10)
        
        self.clear_btn = tk.Button(btn_frame, text="清除结果", command=self.clear_results, 
                                  bg="#666", fg="white", font=("Arial", 12), width=15)
        self.clear_btn.pack(side="left", padx=10)
        
        self.progress = ttk.Progressbar(self.root, mode="indeterminate")
        self.progress.pack(pady=10, padx=20, fill="x")
        
        result_frame = tk.Frame(self.root, bg="#f5f5f5")
        result_frame.pack(pady=10, padx=20, fill="both", expand=True)
        
        tk.Label(result_frame, text="提取结果:", font=("Arial", 12, "bold"), bg="#f5f5f5").pack(anchor="w")
        self.result_text = scrolledtext.ScrolledText(result_frame, height=20, width=100, font=("Arial", 10))
        self.result_text.pack(pady=10, fill="both", expand=True)
        
        self.status_var = tk.StringVar(value="就绪")
        status_bar = tk.Label(self.root, textvariable=self.status_var, relief="sunken", anchor="w", 
                             font=("Arial", 10), bg="white", fg="#333")
        status_bar.pack(side="bottom", fill="x")
    
    def load_stealth_js(self):
        try:
            if os.path.exists(self.stealth_js_path):
                with open(self.stealth_js_path, 'r', encoding='utf-8') as f:
                    return f.read()
            raise FileNotFoundError(f"stealth.min.js文件 {self.stealth_js_path} 不存在")
        except Exception as e:
            self.root.after(0, self.show_error, f"加载stealth.min.js失败: {str(e)}")
            return None
    
    def save_cookies(self, cookies):
        try:
            with open(self.cookie_file, 'w', encoding='utf-8') as f:
                json.dump(cookies, f, ensure_ascii=False, indent=2)
        except Exception as e:
            self.root.after(0, self.show_error, f"保存Cookies失败: {str(e)}")
    
    def load_cookies(self):
        try:
            if os.path.exists(self.cookie_file):
                with open(self.cookie_file, 'r', encoding='utf-8') as f:
                    return json.load(f)
            return None
        except Exception as e:
            self.root.after(0, self.show_error, f"加载Cookies失败: {str(e)}")
            return None
    
    def login_and_get_cookies(self):
        try:
            self.status_var.set("正在访问登录页面...")
            self.driver.get(self.login_url)
            
            # 应用stealth.min.js
            stealth_js = self.load_stealth_js()
            if stealth_js:
                self.driver.execute_script(stealth_js)
            else:
                raise Exception("无法加载stealth.min.js")
            
            self.status_var.set("请在30秒内完成扫码登录...")
            time.sleep(30)
            
            WebDriverWait(self.driver, 10).until(
                EC.url_contains("jd.com")
            )
            
            self.status_var.set("登录成功，正在保存Cookies...")
            cookies = self.driver.get_cookies()
            self.save_cookies(cookies)
            return cookies
        except Exception as e:
            self.root.after(0, self.show_error, f"登录失败: {str(e)}")
            return None
    
    def simulate_human_behavior(self):
        try:
            # 随机滚动
            for _ in range(random.randint(2, 5)):
                scroll_distance = random.randint(200, 800)
                self.driver.execute_script(f"window.scrollBy(0, {scroll_distance});")
                time.sleep(random.uniform(0.5, 2))
            
            # 随机鼠标移动
            action = ActionChains(self.driver)
            action.move_by_offset(random.randint(50, 200), random.randint(50, 200)).perform()
            time.sleep(random.uniform(0.3, 1))
        except Exception as e:
            self.root.after(0, self.show_error, f"模拟用户行为失败: {str(e)}")
    
    def save_to_excel(self, data):
        wb = openpyxl.Workbook()
        ws = wb.active
        ws.title = "JD Data"
        ws.append(["商品编号", "标题", "价格", "销量", "店铺名称"])
        
        for item in data:
            ws.append([item["index"], item["title"], item["price"], item["sales"], item["shop"]])
        
        wb.save(self.output_file)
    
    def start_crawling(self):
        self.start_btn.config(state="disabled")
        self.progress.start(10)
        self.status_var.set("正在初始化浏览器...")
        
        thread = Thread(target=self.crawl_data)
        thread.daemon = True
        thread.start()
    
    def crawl_data(self):
        max_retries = 3
        retry_delay = 5
        
        for attempt in range(max_retries):
            try:
                self.driver = webdriver.Chrome(options=self.chrome_options)
                
                # 应用stealth.min.js
                stealth_js = self.load_stealth_js()
                if stealth_js:
                    self.driver.execute_script(stealth_js)
                else:
                    raise Exception("无法加载stealth.min.js")
                
                self.status_var.set("正在加载Cookies...")
                cookies = self.load_cookies()
                
                if not cookies:
                    self.status_var.set("Cookies文件不存在或无效，正在尝试登录...")
                    cookies = self.login_and_get_cookies()
                    if not cookies:
                        raise Exception("无法获取有效的Cookies")
                
                self.status_var.set("正在设置Cookies...")
                self.driver.get("https://search.jd.com")
                for cookie in cookies:
                    try:
                        if 'domain' in cookie and 'jd.com' in cookie['domain']:
                            self.driver.add_cookie(cookie)
                        else:
                            self.root.after(0, self.show_error, f"跳过无效Cookie: {cookie.get('name', '未知')}")
                    except Exception as e:
                        self.root.after(0, self.show_error, f"设置Cookie失败: {str(e)}")
                
                self.status_var.set("正在加载搜索页面...")
                self.driver.get(self.url_var.get())
                
                WebDriverWait(self.driver, 10).until(
                    EC.presence_of_element_located((By.CSS_SELECTOR, "div[class*='_wrapper_2xp6d_3']"))
                )
                
                self.status_var.set("正在模拟用户行为...")
                self.simulate_human_behavior()
                
                self.status_var.set("正在提取商品信息...")
                containers = self.driver.find_elements(By.CSS_SELECTOR, "div[class*='_wrapper_2xp6d_3']")
                
                results = []
                data = []
                if not containers:
                    results.append("未找到商品容器，可能是Cookies无效或页面未加载正确\n")
                else:
                    for i, container in enumerate(containers[:10]):
                        try:
                            title_elem = container.find_element(By.CLASS_NAME, "_text_1x4i2_30")
                            title = title_elem.text.strip() or "标题不可用"
                            
                            price_elem = container.find_element(By.CLASS_NAME, "_price_1tn4o_13")
                            price = price_elem.text.strip() or "价格不可用"
                            
                            sales_elem = container.find_element(By.CLASS_NAME, "_goods_volume_1xkku_1")
                            sales = sales_elem.find_element(By.CSS_SELECTOR, "span[title]").text.strip() or "销量不可用"
                            
                            shop_elem = container.find_element(By.CLASS_NAME, "_name_d19t5_35")
                            shop = shop_elem.find_element(By.TAG_NAME, "span").text.strip() or "店铺不可用"
                            
                            results.append(f"商品 {i+1}:\n标题: {title}\n价格: {price}\n销量: {sales}\n店铺名称: {shop}\n{'-'*50}\n")
                            data.append({"index": i+1, "title": title, "price": price, "sales": sales, "shop": shop})
                        except Exception as e:
                            results.append(f"商品 {i+1} 提取失败: {str(e)}\n")
                            data.append({"index": i+1, "title": "提取失败", "price": "提取失败", "sales": "提取失败", "shop": str(e)})
                
                if data:
                    self.status_var.set("正在导出到Excel...")
                    self.save_to_excel(data)
                    results.append(f"数据已导出到 {self.output_file}\n")
                
                self.root.after(0, self.update_results, results)
                break
            
            except Exception as e:
                error_msg = f"尝试 {attempt + 1}/{max_retries} 失败: {str(e)}"
                self.root.after(0, self.show_error, error_msg)
                if attempt < max_retries - 1:
                    self.status_var.set(f"等待 {retry_delay} 秒后重试...")
                    time.sleep(retry_delay)
                else:
                    self.root.after(0, self.show_error, f"提取失败，已达到最大重试次数: {str(e)}")
            
            finally:
                if self.driver:
                    self.driver.quit()
                    self.driver = None
        
        self.root.after(0, self.crawling_finished)
    
    def update_results(self, results):
        self.result_text.delete(1.0, tk.END)
        for result in results:
            self.result_text.insert(tk.END, result)
        self.status_var.set(f"提取完成，共找到 {len(results)} 个记录")
    
    def show_error(self, error_msg):
        self.result_text.delete(1.0, tk.END)
        self.result_text.insert(tk.END, error_msg)
        self.status_var.set("提取失败")
    
    def crawling_finished(self):
        self.progress.stop()
        self.start_btn.config(state="normal")
    
    def clear_results(self):
        self.result_text.delete(1.0, tk.END)
        self.status_var.set("已清除结果")
    
    def __del__(self):
        if self.driver:
            self.driver.quit()

if __name__ == "__main__":
    root = tk.Tk()
    app = JDItemCrawlerApp(root)
    root.mainloop()